﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information
// 
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to you under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
// 
// http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// 

using System;
#if !NETCF && !NETSTANDARD1_3
using System.Collections;
using System.Diagnostics;

using log4net.Util;
#endif

namespace log4net.Core
{
    /// <summary>The internal representation of caller location information.</summary>
    /// <remarks>
    /// <para>
    /// This class uses the <c>System.Diagnostics.StackTrace</c> class to generate
    /// a call stack. The caller's information is then extracted from this stack.
    /// </para>
    /// <para>
    /// The <c>System.Diagnostics.StackTrace</c> class is not supported on the 
    /// .NET Compact Framework 1.0 therefore caller location information is not
    /// available on that framework.
    /// </para>
    /// <para>
    /// The <c>System.Diagnostics.StackTrace</c> class has this to say about Release builds:
    /// </para>
    /// <para>
    /// "StackTrace information will be most informative with Debug build configurations. 
    /// By default, Debug builds include debug symbols, while Release builds do not. The 
    /// debug symbols contain most of the file, method name, line number, and column 
    /// information used in constructing StackFrame and StackTrace objects. StackTrace 
    /// might not report as many method calls as expected, due to code transformations 
    /// that occur during optimization."
    /// </para>
    /// <para>
    /// This means that in a Release build the caller information may be incomplete or may 
    /// not exist at all! Therefore caller location information cannot be relied upon in a Release build.
    /// </para>
    /// </remarks>
    /// <author>Nicko Cadell</author>
    /// <author>Gert Driesen</author>
#if !NETCF
    [Serializable]
#endif
    public class LocationInfo
    {
        /// <summary>Constructor</summary>
        /// <param name="callerStackBoundaryDeclaringType">The declaring type of the method that is
        /// the stack boundary into the logging system for this call.</param>
        /// <remarks>
        /// <para>
        /// Initializes a new instance of the <see cref="LocationInfo" />
        /// class based on the current thread.
        /// </para>
        /// </remarks>
        public LocationInfo(Type callerStackBoundaryDeclaringType) 
        {
            // Initialize all fields
            this.m_className = NA;
            this.m_fileName = NA;
            this.m_lineNumber = NA;
            this.m_methodName = NA;
            this.m_fullInfo = NA;

#if !NETCF && !NETSTANDARD1_3 // StackTrace isn't fully implemented for NETSTANDARD1_3 https://github.com/dotnet/corefx/issues/1797
            if (callerStackBoundaryDeclaringType != null)
            {
                try
                {
                    StackTrace st = new StackTrace(true);
                    int frameIndex = 0;
                                                                                
                    // skip frames not from fqnOfCallingClass
                    while (frameIndex < st.FrameCount)
                    {
                        StackFrame frame = st.GetFrame(frameIndex);
                        if (frame != null && frame.GetMethod().DeclaringType == callerStackBoundaryDeclaringType)
                        {
                            break;
                        }
                        frameIndex++;
                    }

                    // skip frames from fqnOfCallingClass
                    while (frameIndex < st.FrameCount)
                    {
                        StackFrame frame = st.GetFrame(frameIndex);
                        if (frame != null && frame.GetMethod().DeclaringType != callerStackBoundaryDeclaringType)
                        {
                            break;
                        }
                        frameIndex++;
                    }

                    if (frameIndex < st.FrameCount)
                    {
                        // take into account the frames we skip above
                        int adjustedFrameCount = st.FrameCount - frameIndex;
                        ArrayList stackFramesList = new ArrayList(adjustedFrameCount);
                        this.m_stackFrames = new StackFrameItem[adjustedFrameCount];
                        for (int i = frameIndex; i < st.FrameCount; i++) 
                        {
                            stackFramesList.Add(new StackFrameItem(st.GetFrame(i)));
                        }
                                                
                        stackFramesList.CopyTo(this.m_stackFrames, 0);
                        
                        // now frameIndex is the first 'user' caller frame
                        StackFrame locationFrame = st.GetFrame(frameIndex);

                        if (locationFrame != null)
                        {
                            System.Reflection.MethodBase method = locationFrame.GetMethod();

                            if (method != null)
                            {
                                this.m_methodName = method.Name;
                                if (method.DeclaringType != null)
                                {
                                    this.m_className = method.DeclaringType.FullName;
                                }
                            }

                            this.m_fileName = locationFrame.GetFileName();
                            this.m_lineNumber = locationFrame.GetFileLineNumber().ToString(System.Globalization.NumberFormatInfo.InvariantInfo);

                            // Combine all location info
                            this.m_fullInfo = this.m_className + '.' + this.m_methodName + '(' + this.m_fileName + ':' + this.m_lineNumber + ')';
                        }
                    }
                }
                catch(System.Security.SecurityException)
                {
                    // This security exception will occur if the caller does not have 
                    // some undefined set of SecurityPermission flags.
                    LogLog.Debug(declaringType, "Security exception while trying to get caller stack frame. Error Ignored. Location Information Not Available.");
                }
            }
#endif
        }

        /// <summary>Constructor</summary>
        /// <param name="className">The fully qualified class name.</param>
        /// <param name="methodName">The method name.</param>
        /// <param name="fileName">The file name.</param>
        /// <param name="lineNumber">The line number of the method within the file.</param>
        /// <remarks>
        /// <para>
        /// Initializes a new instance of the <see cref="LocationInfo" />
        /// class with the specified data.
        /// </para>
        /// </remarks>
        public LocationInfo(string className, string methodName, string fileName, string lineNumber)
        {
            this.m_className = className;
            this.m_fileName = fileName;
            this.m_lineNumber = lineNumber;
            this.m_methodName = methodName;
            this.m_fullInfo = this.m_className + '.' + this.m_methodName + '(' + this.m_fileName + 
                              ':' + this.m_lineNumber + ')';
        }

        /// <summary>
        /// Gets the fully qualified class name of the caller making the logging 
        /// request.
        /// </summary>
        /// <value>
        /// The fully qualified class name of the caller making the logging 
        /// request.
        /// </value>
        /// <remarks>
        /// <para>
        /// Gets the fully qualified class name of the caller making the logging 
        /// request.
        /// </para>
        /// </remarks>
        public string ClassName
        {
            get { return this.m_className; }
        }

        /// <summary>Gets the file name of the caller.</summary>
        /// <value>
        /// The file name of the caller.
        /// </value>
        /// <remarks>
        /// <para>
        /// Gets the file name of the caller.
        /// </para>
        /// </remarks>
        public string FileName
        {
            get { return this.m_fileName; }
        }

        /// <summary>Gets the line number of the caller.</summary>
        /// <value>
        /// The line number of the caller.
        /// </value>
        /// <remarks>
        /// <para>
        /// Gets the line number of the caller.
        /// </para>
        /// </remarks>
        public string LineNumber
        {
            get { return this.m_lineNumber; }
        }

        /// <summary>Gets the method name of the caller.</summary>
        /// <value>
        /// The method name of the caller.
        /// </value>
        /// <remarks>
        /// <para>
        /// Gets the method name of the caller.
        /// </para>
        /// </remarks>
        public string MethodName
        {
            get { return this.m_methodName; }
        }

        /// <summary>Gets all available caller information</summary>
        /// <value>
        /// All available caller information, in the format
        /// <c>fully.qualified.classname.of.caller.methodName(Filename:line)</c>
        /// </value>
        /// <remarks>
        /// <para>
        /// Gets all available caller information, in the format
        /// <c>fully.qualified.classname.of.caller.methodName(Filename:line)</c>
        /// </para>
        /// </remarks>
        public string FullInfo
        {
            get { return this.m_fullInfo; }
        }
        
#if !NETCF && !NETSTANDARD1_3
        /// <summary>Gets the stack frames from the stack trace of the caller making the log request</summary>
        public StackFrameItem[] StackFrames
        {
            get { return this.m_stackFrames; }
        }
#endif

        private readonly string m_className;
        private readonly string m_fileName;
        private readonly string m_lineNumber;
        private readonly string m_methodName;
        private readonly string m_fullInfo;
#if !NETCF && !NETSTANDARD1_3
        private readonly StackFrameItem[] m_stackFrames;
#endif

        /// <summary>The fully qualified type of the LocationInfo class.</summary>
        /// <remarks>
        /// Used by the internal logger to record the Type of the
        /// log message.
        /// </remarks>
        private static readonly Type declaringType = typeof(LocationInfo);

        /// <summary>
        /// When location information is not available the constant
        /// <c>NA</c> is returned. Current value of this string
        /// constant is <b>?</b>.
        /// </summary>
        private const string NA = "?";
    }
}
